<?php

// ------------------------------------------------------------------------
// |@Author       : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Date         : 2025-05-05 07:27:35
// |@----------------------------------------------------------------------
// |@LastEditTime : 2025-05-11 14:25:46
// |@----------------------------------------------------------------------
// |@LastEditors  : Jarmin <topextend@qq.com>
// |@----------------------------------------------------------------------
// |@Description  :
// |@----------------------------------------------------------------------
// |@FilePath     : LoadMultiApp.php
// |@----------------------------------------------------------------------
// |@Copyright (c) 2025 http://www.ladmin.cn All Rights Reserved.
// ------------------------------------------------------------------------
declare (strict_types=1);

namespace think\admin\middleware;

use Closure;
use think\App;
use think\Request;
use think\Response;
use think\exception\HttpException;
use think\admin\traits\HttpStatusTrait;

/**
 * 多应用中间件
 *
 * 核心功能：
 * 1. 支持多应用模式下的应用自动识别（基于路径参数和域名绑定）
 * 2. 支持子域名绑定应用（精确域名匹配和通配符匹配）
 * 3. 应用映射（别名）和禁用列表（黑名单）管理
 * 4. 自动加载应用资源（配置/路由/事件/服务等）
 * 5. 运行时目录隔离（每个应用独立runtime目录）
 *
 * @package think\admin\middleware
 * @version 1.3.0
 */
class LoadMultiApp
{
    use HttpStatusTrait;

    /**
     * @var App ThinkPHP应用实例
     *          通过依赖注入自动获取
     */
    protected $app;

    /**
     * 构造函数（依赖注入）
     *
     * 设计说明：
     * 1. 通过容器自动注入App实例
     * 2. 支持中间件在整个请求生命周期中共享应用实例
     *
     * @param App $app 框架核心实例（自动依赖注入）
     *                 包含配置管理、服务容器等核心功能
     */
    public function __construct(App $app)
    {
        $this->app = $app;
    }

    /**
     * 中间件入口方法
     *
     * 执行流程：
     * 1. 解析多应用配置（自动识别或绑定应用）
     * 2. 加载应用相关资源（配置/路由/服务等）
     * 3. 初始化应用中间件管道
     *
     * @param Request $request 注入版本属性到请求对象
     * @return Response 应用处理后的响应
     *
     * @throws BusinessException 应用不存在时抛出404异常
     */
    public function handle($request, Closure $next)
    {
        try {
            // 解析多应用配置
            if (!$this->parseMultiApp()) {
                return $next($request);
            }

            // 执行应用中间件管道
            return $this->app->middleware->pipeline('app')
                ->send($request)
                ->then(function ($request) use ($next) {
                    return $next($request);
                });
        } catch (\Exception $e) {
            return json([
                'code' => $e->getCode() ?: 400,
                'message' => $e->getMessage(),
                'data' => $e->getErrorData(),
                'timestamp' => time()
            ]);
        }
    }

    /**
     * 获取路由目录路径
     *
     * @return string 路由目录绝对路径
     */
    protected function getRoutePath(): string
    {
        return $this->app->getAppPath() . 'route' . DIRECTORY_SEPARATOR;
    }

    /**
     * 解析多应用配置（核心逻辑）
     *
     * 域名匹配优先级：
     * 1. 完整域名匹配（如：www.example.com）
     * 2. 子域名匹配（如：admin.example.com）
     * 3. 通配符匹配（*）
     *
     * 路径解析规则：
     * 1. 从路径第一段获取应用名称（如：/admin/user -> admin）
     * 2. 处理带扩展名的应用名称（如：admin.php -> admin）
     * 3. 应用映射别名转换
     *
     * @return bool 是否成功加载应用
     * @throws BusinessException 应用被禁用或不存在时抛出
     */
    protected function parseMultiApp(): bool
    {
        // 获取入口脚本名称（不带扩展名）
        $scriptName = $this->getScriptName();
        // 从配置获取默认应用名称（默认为index）
        $defaultApp = $this->app->config->get('app.default_app') ?: 'index';
        // 从HTTP对象获取当前应用名称（可能为null）
        $appName    = $this->app->http->getName();

        // 强制绑定模式处理（通过入口脚本或手动绑定）
        if ($appName || ($scriptName && !in_array($scriptName, ['index', 'router', 'think']))) {
            // 优先使用手动绑定的应用名称
            $appName = $appName ?: $scriptName;
            $this->app->http->setBind();
        } else {
            // 自动识别模式
            $this->app->http->setBind(false);
            $appName = null;

            // 获取域名绑定配置（支持精确域名和通配符）
            $bind = $this->app->config->get('app.domain_bind', []);

            // 域名绑定处理逻辑
            if (!empty($bind)) {
                $subDomain = $this->app->request->subDomain();
                $domain    = $this->app->request->host(true);

                // 域名匹配优先级：完整域名 > 子域名 > 通配符
                if (isset($bind[$domain])) {
                    $appName = $bind[$domain];
                    $this->app->http->setBind();
                } elseif (isset($bind[$subDomain])) {
                    $appName = $bind[$subDomain];
                    $this->app->http->setBind();
                } elseif (isset($bind['*'])) {
                    $appName = $bind['*'];
                    $this->app->http->setBind();
                }
            }

            // 未绑定域名时的路径解析逻辑
            if (!$this->app->http->isBind()) {
                $path = $this->app->request->pathinfo();
                $map  = $this->app->config->get('app.app_map', []);
                $deny = $this->app->config->get('app.deny_app_list', []);

                // 从路径中提取第一个段作为应用名称
                $name = current(explode('/', $path));

                // 处理带扩展名的应用名称（如admin.php -> admin）
                if (strpos($name, '.')) {
                    $name = strstr($name, '.', true);
                }

                // 应用映射处理逻辑
                if (isset($map[$name])) {
                    // 动态映射处理（闭包方式）
                    if ($map[$name] instanceof Closure) {
                        $result  = call_user_func_array($map[$name], [$this->app]);
                        $appName = $result ?: $name;
                    } else {
                        // 静态映射处理
                        $appName = $map[$name];
                    }
                } elseif ($name && (false !== array_search($name, $map) || in_array($name, $deny))) {
                    throw new HttpException(self::HTTP_NOT_FOUND, lang('system.app_not_exists', ['name' => $appName]));
                } elseif ($name && isset($map['*'])) {
                    // 通配符映射处理
                    $appName = $map['*'];
                } else {
                    // 默认应用处理逻辑
                    $appName = $name ?: $defaultApp;
                    $appPath = $this->app->http->getPath() ?: $this->app->getBasePath() . $appName . DIRECTORY_SEPARATOR;

                    // 应用目录检查（快速模式/严格模式）
                    if (!is_dir($appPath)) {
                        $express = $this->app->config->get('app.app_express', false);
                        if ($express) {
                            // 快速模式：直接使用默认应用
                            $this->setApp($defaultApp);
                            return true;
                        } else {
                            // 严格模式：返回解析失败
                            return false;
                        }
                    }
                }

                // 路径重写处理
                if ($name) {
                    $this->app->request->setRoot('/' . $name);
                    $this->app->request->setPathinfo(strpos($path, '/') ? ltrim(strstr($path, '/'), '/') : '');
                }
            }
        }

        // 最终应用设置
        $this->setApp($appName ?: $defaultApp);
        return true;
    }

    /**
     * 获取当前运行入口文件名
     *
     * 实现逻辑：
     * 1. 优先从SCRIPT_FILENAME获取当前执行脚本绝对路径
     * 2. CLI模式下通过argv[0]获取执行文件路径
     * 3. 使用pathinfo提取不含扩展名的文件名
     *
     * @return string 入口文件名(不含扩展名)
     *
     * @example
     * Web访问：/public/index.php -> 返回 "index"
     * CLI执行：php admin.php -> 返回 "admin"
     */
    protected function getScriptName(): string
    {
        // 优先从服务器变量获取脚本路径
        if (isset($_SERVER['SCRIPT_FILENAME'])) {
            $file = $_SERVER['SCRIPT_FILENAME'];
        }
        // CLI模式下从命令行参数获取执行文件
        elseif (isset($_SERVER['argv'][0])) {
            $file = realpath($_SERVER['argv'][0]);
        }

        // 提取文件名并去除扩展名
        return isset($file) ? pathinfo($file, PATHINFO_FILENAME) : '';
    }

    /**
     * 设置当前应用运行环境
     *
     * 初始化操作：
     * 1. 设置应用路径和命名空间
     * 2. 创建独立运行时目录
     * 3. 加载应用配置文件和服务
     *
     * @param string $appName 应用名称（小写）
     */
    protected function setApp(string $appName): void
    {
        // 设置应用名称
        $this->app->http->name($appName);

        // 计算应用路径（优先使用配置路径，默认在base_path下）
        $appPath = $this->app->http->getPath() ?: $this->app->getBasePath() . $appName . DIRECTORY_SEPARATOR;

        // 设置应用基础路径
        $this->app->setAppPath($appPath);
        // 设置应用命名空间（默认格式：app\{应用名}）
        $this->app->setNamespace($this->app->config->get('app.app_namespace') ?: 'app\\' . $appName);

        // 应用目录存在时的初始化操作
        if (is_dir($appPath)) {
            // 设置运行时目录（runtime/{应用名}/）
            $this->app->setRuntimePath($this->app->getRuntimePath() . $appName . DIRECTORY_SEPARATOR);
            // 设置路由目录
            $this->app->http->setRoutePath($this->getRoutePath());

            // 加载应用资源
            $this->loadApp($appName, $appPath);
        }
    }

    /**
     * 加载应用资源文件
     *
     * 加载顺序：
     * 1. 公共函数文件（common.php）
     * 2. 配置文件（config目录）
     * 3. 事件监听器（event.php）
     * 4. 中间件定义（middleware.php）
     * 5. 服务提供者（provider.php）
     * 6. 语言包文件（lang目录）
     *
     * 安全提示：
     * - 生产环境建议禁用自动加载未签名配置文件
     * - 敏感配置建议放在不可Web访问的目录
     */
    protected function loadApp(string $appName, string $appPath): void
    {
        // 加载公共函数文件（优先于所有配置文件）
        if (is_file($appPath . 'common.php')) {
            include_once $appPath . 'common.php';
        }

        $files = [];
        // 合并所有配置文件（*.php）
        $files = array_merge($files, glob($appPath . 'config' . DIRECTORY_SEPARATOR . '*' . $this->app->getConfigExt()));

        // 加载配置文件到应用配置（按文件名作为配置域）
        foreach ($files as $file) {
            $this->app->config->load($file, pathinfo($file, PATHINFO_FILENAME));
        }

        // 注册应用事件监听器
        if (is_file($appPath . 'event.php')) {
            $this->app->loadEvent(include $appPath . 'event.php');
        }

        // 导入应用中间件定义
        if (is_file($appPath . 'middleware.php')) {
            $this->app->middleware->import(include $appPath . 'middleware.php', 'app');
        }

        // 绑定服务提供者
        if (is_file($appPath . 'provider.php')) {
            $this->app->bind(include $appPath . 'provider.php');
        }

        // 加载当前语言包（根据config->get('lang.default_lang')）
        $this->app->loadLangPack($this->app->lang->getLangSet());
    }
}
